#include <err.h>
#include <fcntl.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/param.h>

#include "../util.h"
#include "../getopt_helper.h"

static struct option const LONG_OPTIONS[] = {
	{ "number-nonblank", no_argument, NULL, 'b' },
	{ "number", no_argument, NULL, 'n' },
	{ "squeeze-blank", no_argument, NULL, 's' },
	{ "show-nonprinting", no_argument, NULL, 'v' },
	{ "show-ends", no_argument, NULL, 'E' },
	{ "show-tabs", no_argument, NULL, 'T' },
	{ "show-all", no_argument, NULL, 'A' },
	GETOPT_END_DECL
};

#define NFORMAT 3
const char *usage_str =
    "Usage: %s [OPTION]... [FILE]...\n"
    "Concatenate FILE(s) to standard output.\n"
    "\n"
    "With no FILE, or when FILE is -, read standard input.\n"
    "\n"
    "  -A, --show-all           equivalent to -vET\n"
    "  -b, --number-nonblank    number nonempty output lines, "
    "overrides -n\n"
    "  -e                       equivalent to -vE\n"
    "  -E, --show-ends          display $ at end of each line\n"
    "  -n, --number             number all output lines\n"
    "  -s, --squeeze-blank      suppress repeated empty output lines\n"
    "  -t                       equivalent to -vT\n"
    "  -T, --show-tabs          display TAB characters as ^I\n"
    "  -u                       (ignored)\n"
    "  -v, --show-nonprinting   use ^ and M- notation, except for LFD "
    "and "
    "TAB\n"
    "      --help               display this help and exit\n"
    "      --version            output version information and exit\n"
    "\n"
    "Examples:\n"
    "  %s f - g  Output f's contents, then standard input, then g's "
    "contents.\n"
    "  %s        Copy standard input to standard output.";

static enum flag INIT_FLAGS(NO, b, E, n, s, T, v);

static int cook_args(char *argv[]);
static int cook_buf(FILE *, char *);
static int raw_args(char *argv[]);
static int raw_cat(int, char *);

/*
 * > As of May 2014, 128KiB is determined to be the minimium blksize to best
 *   minimize system call overhead.
 *
 * detailed explained in `gnu-coreutils/src/ioblksize.h`
 */
enum { IO_BUFSIZE = 128 * 1024 };

int
main(int argc, char *argv[])
{
	int ch, ret;

	while ((ch = getopt_long(argc, argv, "benstuvAET", LONG_OPTIONS, NULL))
	       != -1) {
		switch (ch) {
			IS_FLAGS(YES, n, s, v, E, T);

		case 'b':
			bflag = nflag = YES;
			break;

		case 'e':
			Eflag = vflag = YES;
			break;

		case 't':
			Tflag = vflag = YES;
			break;

		case 'u':
			setvbuf(stdout, NULL, _IONBF, 0);
			break;

		case 'A':
			vflag = Eflag = Tflag = YES;
			break;

			GETOPT_END_CASE
		}
	}

	argv += optind;

	if (bflag || Eflag || nflag || sflag || Tflag || vflag)
		ret = cook_args(argv);
	else
		ret = raw_args(argv);

	if (fclose(stdout)) err(1, "stdout");

	return ret;
}

int
cook_args(char *argv[])
{
	int   ret;
	FILE *fptr     = stdin;
	char *filename = "<stdin>";

	do {
		if (*argv) {
			if (STREQ(*argv, "-")) {
				fptr = stdin;
			} else if (!(fptr = fopen(*argv, "re"))) {
				warn("%s", *argv);
				ret = !0;
				argv++;
				continue;
			}
			filename = *argv++;
		}

		ret = cook_buf(fptr, filename);

		if (fptr == stdin)
			clearerr(fptr);
		else
			fclose(fptr);

	} while (*argv);

	return ret;
}

int
cook_buf(FILE *fptr, char *filename)
/* TODO: optimize (use buffer) */
{
	int ret = 0;
	int ch, gobble, line, prev;

	line = gobble = 0;

	for (prev = '\n'; (ch = getc(fptr)) != EOF; prev = ch) {
		if (prev == '\n') {
			if (sflag) {
				if (ch == '\n') {
					if (gobble) continue;
					gobble = !0;
				} else {
					gobble = 0;
				}
			}
			if (nflag) {
				if (!bflag || ch != '\n')
					printf("%6d\t", ++line);
				else if (Eflag)
					printf("%6s", "");

				if (ferror(stdout)) break;
			}
		}

		if (ch == '\n') {
			if (Eflag && putchar('$') == EOF) break;
		} else if (ch == '\t') {
			if (Tflag) {
				if (putchar('^') == EOF || putchar('I') == EOF)
					break;
				continue;
			}
		} else if (vflag) {
			if (!isascii(ch)) {
				if (putchar('M') == EOF || putchar('-') == EOF)
					break;
				ch = toascii(ch);
			}

			if (iscntrl(ch)) {
				if (putchar('^') == EOF
				    || putchar(ch == '\177' ? '?' : ch | 0100)
					   == EOF)
					break;
				continue;
			}
		}
		if (putchar(ch) == EOF) break;
	}

	if (ferror(fptr)) {
		warn("%s", filename);
		ret = 1;
		clearerr(fptr);
	}

	if (ferror(stdout)) err(!0, "stdout");

	return ret;
}

int
raw_args(char **argv)
{
	char *filename = "<stdin>";
	int   ret, fd = fileno(stdin);

	do {
		if (*argv) {
			if (STREQ(*argv, "-"))
				fd = fileno(stdin);
			else if ((fd = open(*argv, O_RDONLY | O_CLOEXEC, 0))
				 == -1) {
				warn("%s", *argv);
				ret = 1;
				argv++;
				continue;
			}
			filename = *argv++;
		}

		ret = raw_cat(fd, filename);

		if (fd != fileno(stdin)) close(fd);
	} while (*argv);

	return ret;
}

int
raw_cat(int rfd, char *filename)
{
	int	      ret = 0;
	int	      wfd = fileno(stdout);
	ssize_t	      nr, nw, off;
	static size_t bsize;
	static char  *buf = NULL;
	struct stat   sbuf;

	if (!buf) {
		if (fstat(wfd, &sbuf) == -1) err(!0, "stdout");

		SAFE_ASSIGN(bsize, MAX(sbuf.st_blksize, IO_BUFSIZE));

		if (!(buf = malloc(bsize))) err(!0, "malloc");
	}

	while ((nr = read(rfd, buf, bsize)) > 0)
		for (off = 0; !!nr; nr -= nw, off += nw)
			if ((nw = write(wfd, buf += off, (size_t)nr)) < 1)
				err(!0, "stdout");

	if (nr == -1) {
		warn("%s", filename);
		ret = 1;
	}

	return ret;
}
